/*
 * // Bristleback plugin - Copyright (c) 2010 bristleback.googlecode.com
 * // ---------------------------------------------------------------------------
 * // This program is free software; you can redistribute it and/or modify it
 * // under the terms of the GNU Lesser General Public License as published by the
 * // Free Software Foundation; either version 3 of the License, or (at your
 * // option) any later version.
 * // This library is distributed in the hope that it will be useful,
 * // but without any warranty; without even the implied warranty of merchantability
 * // or fitness for a particular purpose.
 * // You should have received a copy of the GNU Lesser General Public License along
 * // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>.
 * // ---------------------------------------------------------------------------
 */

package pl.bristleback.server.bristle.token;

import org.apache.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import org.jwebsocket.token.JSONToken;
import org.jwebsocket.token.Token;
import pl.bristleback.server.bristle.exceptions.BristleRuntimeException;
import pl.bristleback.server.bristle.exceptions.BristleTokenException;
import pl.bristleback.server.bristle.utils.JsonUtil;
import pl.bristleback.server.bristle.utils.StringUtil;

import java.util.HashMap;
import java.util.Map;

/**
 * This class allows adding raw and complex objects to Token.
 * Lists and arrays will be changed into <code>JSONArray</code> objects, maps will be changed into <code>JSONObject</code> objects.
 * Complex custom objects will be changed into <code>JSONObject</code> using reflection.
 * <code>deepSearch</code> parameter specifies whether reflection searching should be recursive
 * in case when another complex object is found inside already processed object.
 * In other words, if object A contains object B and
 * <code>deepSearch</code> parameter is set to true, it tell us that object B should be also added to token.
 * Moreover, parameter names can be complex, like <code>parent.nestedObject</code>.
 * In that case, inside token, there will be <code>JSONObject</code> object created with name <code>parent</code>.
 * That object will contain <code>nestedObject</code> parameter.
 * <p/>
 * This is a default implementation of {@link TokenizerEngine} interface,
 * used when custom object tokenizer is not set in {@link Tokenize} annotation.
 * Currently, this implementation works only for JSONToken objects,
 * because of big differences in functionality in various Token implementations.
 * <p/>
 * Created on: 2010-12-16 14:29:14 <br/>
 *
 * @author Wojciech Niemiec
 */
public class JsonTokenizerEngine implements TokenizerEngine {
  private static Logger log = Logger.getLogger(JsonTokenizerEngine.class.getName());

  private static final int NO_DOTS = -1;

  /**
   * Adds raw and complex objects to token. Method accepts only <code>JSONToken</code> implementation of token.
   *
   * @param token          a token to fill.
   * @param deepSearch     if set to true, reflection searching of complex objects will be recursive.
   * @param parameterNames keys of token entries. Can be complex names, with dots as separators,
   *                       like <code>object.nestedObject.moreNestedObject</code>.
   * @param values         values of token entries. Can be raw or complex objects, maps, lists and arrays.
   * @return token given as parameter with filled parameters.
   */
  public Token tokenize(Token token, boolean deepSearch, String[] parameterNames, Object[] values) {
    if (!(token instanceof JSONToken)) {
      throw new BristleTokenException("Only " + JSONToken.class.getSimpleName() + " Token implementation is allowed."
        + "\n Token implementations in Jwebsocket are incompatible with each other.");
    }
    addToJSONToken((JSONToken) token, deepSearch, parameterNames, values);
    return token;
  }

  private void addToJSONToken(JSONToken token, boolean deepSearch, String[] parameterNames, Object[] values) {
    try {
      for (int i = 0; i < parameterNames.length; i++) {
        Object processedValue = JsonUtil.processObject(values[i], deepSearch);
        addValueToToken(token, parameterNames[i], processedValue);
      }
    } catch (JSONException e) {
      throw new BristleRuntimeException("Error with JSON: " + e.getMessage(), e);
    }

  }

  private void addValueToToken(JSONToken token, String parameterName, Object processedValue) throws JSONException {
    if (parameterName.indexOf(StringUtil.DOT) == NO_DOTS) {
      token.put(parameterName, processedValue);
    } else {
      addValueAsNestedProperty(token, parameterName, processedValue);
    }
  }

  private void addValueAsNestedProperty(JSONToken token, String parameterName, Object processedValue) throws JSONException {
    String[] parameters = parameterName.split("\\" + StringUtil.DOT);
    JSONObject map = createNestedMaps(token, parameters);
    map.put(getLastParameter(parameters), processedValue);
  }

  public String getLastParameter(String[] parameters) {
    return parameters[parameters.length - 1];
  }

  private JSONObject createNestedMaps(JSONToken token, String[] parameters) throws JSONException {
    JSONObject map = getFirstMap(token, parameters[0]);
    for (int i = 1; i < parameters.length - 1; i++) {
      map = getPropertyMap(map, parameters[i]);
    }
    return map;
  }

  private JSONObject getFirstMap(JSONToken token, String parameter) {
    if (token.get(parameter) == null) {
      Map<String, Object> map = new HashMap<String, Object>();
      token.setMap(parameter, map);
    }
    return (JSONObject) token.get(parameter);
  }

  private JSONObject getPropertyMap(JSONObject parentMap, String parameter) throws JSONException {
    if (!parentMap.has(parameter)) {
      Map<String, Object> nestedMap = new HashMap<String, Object>();
      parentMap.put(parameter, nestedMap);
    }
    return (JSONObject) parentMap.get(parameter);
  }
}
